/*
 * Copyright (c) 2011, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.wso2.carbon.bpel.ode.integration;

import org.apache.axiom.om.OMAbstractFactory;
import org.apache.axiom.soap.SOAP11Constants;
import org.apache.axiom.soap.SOAP12Constants;
import org.apache.axiom.soap.SOAPEnvelope;
import org.apache.axiom.soap.SOAPFactory;
import org.apache.axis2.AxisFault;
import org.apache.axis2.Constants;
import org.apache.axis2.addressing.EndpointReference;
import org.apache.axis2.client.OperationClient;
import org.apache.axis2.client.ServiceClient;
import org.apache.axis2.context.ConfigurationContext;
import org.apache.axis2.context.MessageContext;
import org.apache.axis2.description.*;
import org.apache.axis2.transport.http.HTTPConstants;
import org.apache.axis2.wsdl.WSDLConstants;
import org.apache.ode.bpel.iapi.PartnerRoleMessageExchange;
import org.apache.ode.il.OMUtils;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.wso2.carbon.bpel.ode.integration.axis2.WSDLAwareSOAPProcessor;
import org.wso2.carbon.bpel.ode.integration.utils.AnonymousServiceFactory;
import org.wso2.carbon.bpel.ode.integration.utils.Messages;

import javax.wsdl.*;
import javax.wsdl.extensions.ExtensibilityElement;
import javax.wsdl.extensions.http.HTTPAddress;
import javax.wsdl.extensions.http.HTTPBinding;
import javax.wsdl.extensions.http.HTTPOperation;
import javax.xml.namespace.QName;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class HTTPBindingHandler {
    private ConfigurationContext configurationContext;
    private AxisService service;
    private QName serviceName;
    private String portName;
    private Binding httpBinding;
    private Map<QName, AxisOperation> operations = new HashMap<QName, AxisOperation>();
    private ServiceClient serviceClient;
    private Definition wsdl;

    public HTTPBindingHandler(ConfigurationContext configurationContext,
                              QName serviceName,
                              String portName,
                              Definition wsdl) throws AxisFault {
        this.configurationContext = configurationContext;
        this.serviceName = serviceName;
        this.portName = portName;
        this.wsdl = wsdl;

        inferBinding();
        populateAxisService();

        serviceClient = new ServiceClient(configurationContext, service);
        serviceClient.getOptions().setTo(new EndpointReference(getServiceLocation(getPortDefinition())));
        serviceClient.getOptions().setUseSeparateListener(false);
    }

    public HTTPBindingResponse invoke(final PartnerRoleMessageExchange partnerRoleMessageExchange,
                       final BPELMessageContext bpelMessageContext) throws AxisFault {

        MessageContext messageContext;

        OperationClient operationClient = serviceClient.createClient(new QName(getTargetNamesapce(),
                partnerRoleMessageExchange.getOperationName()));
        operationClient.getOptions().setAction("\"\"");
        operationClient.getOptions().setExceptionToBeThrownOnSOAPFault(true);

        addPropertyToOperationClient(operationClient, WSDL2Constants.ATTR_WHTTP_QUERY_PARAMETER_SEPARATOR, "&");
        addPropertyToOperationClient(operationClient, Constants.Configuration.ENABLE_REST, true);
        addPropertyToOperationClient(operationClient, Constants.Configuration.HTTP_METHOD, getVerb().trim());
        addPropertyToOperationClient(operationClient,
                WSDL2Constants.ATTR_WHTTP_LOCATION,
                getHTTPLocation(partnerRoleMessageExchange.getOperationName()));
        addPropertyToOperationClient(operationClient, Constants.Configuration.CONTENT_TYPE, inferContentType(getVerb()));
        addPropertyToOperationClient(operationClient, Constants.Configuration.MESSAGE_TYPE, inferContentType(getVerb()));

        SOAPEnvelope soapEnvelope = getFactory(operationClient.getOptions().getSoapVersionURI()).getDefaultEnvelope();
        messageContext = new MessageContext();

        populateSOAPBody(soapEnvelope, partnerRoleMessageExchange);
        serviceClient.addHeadersToEnvelope(soapEnvelope);
        messageContext.setEnvelope(soapEnvelope);

        operationClient.addMessageContext(messageContext);

        operationClient.execute(true);

        MessageContext responseMessageContext = operationClient.getMessageContext(WSDLConstants.MESSAGE_LABEL_IN_VALUE);
        MessageContext faultMessageContext = operationClient.getMessageContext(WSDLConstants.MESSAGE_LABEL_FAULT_VALUE);

        return new HTTPBindingResponse(responseMessageContext, faultMessageContext);
    }


    private void populateSOAPBody(SOAPEnvelope soapEnvelope, PartnerRoleMessageExchange partnerRoleMessageExchange) {
        org.apache.ode.bpel.iapi.Message messageToSend = partnerRoleMessageExchange.getRequest();
        if (messageToSend.getParts().size() == 1) {
            soapEnvelope.getBody().addChild(OMUtils.toOM(getPartContent(messageToSend.getPart(messageToSend.getParts().get(0))), soapEnvelope.getOMFactory()));
        } else {
            throw new IllegalArgumentException("HTTP Binding doesn't support multiple message part as the input.");
        }
    }

    private Element getPartContent(Element part){
        if(part.getFirstChild().getNodeType() == Node.ELEMENT_NODE){
            return (Element)part.getFirstChild();
        }

        throw new IllegalArgumentException("Cannot find message content in part element of ODE request to " +
                "the external service.");
    }

    private static SOAPFactory getFactory(String soapVersionURI) {

        if (SOAP11Constants.SOAP_ENVELOPE_NAMESPACE_URI.equals(soapVersionURI)) {
            return OMAbstractFactory.getSOAP11Factory();
        } else if (SOAP12Constants.SOAP_ENVELOPE_NAMESPACE_URI.equals(soapVersionURI)) {
            return OMAbstractFactory.getSOAP12Factory();
        } else {
            throw new RuntimeException(org.apache.axis2.i18n.Messages
                    .getMessage("unknownsoapversion"));
        }
    }

    private String getVerb() {
        ExtensibilityElement extElement = WSDLAwareSOAPProcessor.getBindingExtension(httpBinding);
        if(extElement instanceof HTTPBinding){
            return ((HTTPBinding)extElement).getVerb();
        }

        throw new IllegalArgumentException("Current binding is not a HTTP Binding.");
    }

    private String inferContentType(String httpVerb) {
        if (httpVerb.trim().equals("GET") || httpVerb.trim().equals("DELETE")) {
            return HTTPConstants.MEDIA_TYPE_X_WWW_FORM;
        }

        return HTTPConstants.MEDIA_TYPE_APPLICATION_XML;
    }

    private String getHTTPLocation(String operationName) {
        for (Object bindingOperation : httpBinding.getBindingOperations()) {
            if (((BindingOperation) bindingOperation).getName().equals(operationName)) {
                List<Object> extElements = ((BindingOperation)bindingOperation).getExtensibilityElements();
                if(extElements.size() == 0){
                    throw new RuntimeException();
                } else if(extElements.size() > 1){
                    throw new RuntimeException();
                } else {
                    return ((HTTPOperation)extElements.get(0)).getLocationURI();
                }
            }
        }

        throw new NullPointerException("HTTP Operation's location attribute is null.");
    }

    private void addPropertyToOperationClient(OperationClient operationClient,
                                              String propertyKey,
                                              Object propertyValue) {
        operationClient.getOptions().setProperty(propertyKey, propertyValue);
    }

    protected void addPropertyToOperationClient(OperationClient operationClient,
                                                String propertyKey,
                                                boolean value) {
        addPropertyToOperationClient(operationClient, propertyKey, new Boolean(value));
    }

    protected void addPropertyToOperationClient(OperationClient operationClient,
                                                String propertyKey,
                                                int value) {
        addPropertyToOperationClient(operationClient, propertyKey, new Integer(value));
    }

    private String getServiceLocation(Port protDefinition) {
        for (Object extElement : getPortDefinition().getExtensibilityElements()) {
            if (extElement instanceof HTTPAddress) {
                return ((HTTPAddress) extElement).getLocationURI();
            }
        }

        throw new NullPointerException("Service Location is null. Cannot find HTTP Address from WSDL definition");
    }

    private Port getPortDefinition() {
        Service serviceDef = wsdl.getService(serviceName);
        if (serviceDef == null) {
            throw new NullPointerException(Messages.msgServiceDefinitionNotFound(
                    serviceName.getLocalPart()));
        }
        return serviceDef.getPort(portName);
    }

    private void inferBinding() {
        Service serviceDef = wsdl.getService(serviceName);
        if (serviceDef == null) {
            throw new NullPointerException(Messages.msgServiceDefinitionNotFound(
                    serviceName.getLocalPart()));
        }
        Port port = serviceDef.getPort(portName);

        if (port == null) {
            throw new NullPointerException(Messages.msgServicePortNotFound(
                    serviceName.getLocalPart(), portName));
        }

        httpBinding = port.getBinding();
        if (httpBinding == null) {
            throw new NullPointerException(Messages.msgBindingNotFound(
                    serviceName.getLocalPart(), portName));
        }
    }

    private void populateAxisService() {
        service = AnonymousServiceFactory.getAnonymousService(serviceName,
                portName,
                configurationContext.getAxisConfiguration());

        String targetNamesapce = getTargetNamesapce();

        List<BindingOperation> bindingOperations = httpBinding.getBindingOperations();
        for (BindingOperation bindingOperation : bindingOperations) {
            String operationName = bindingOperation.getName();
            AxisOperation operation = createAxisOperation(operationName, targetNamesapce, isOutOnly(bindingOperation));
            operations.put(new QName(targetNamesapce, operationName), operation);
            service.addOperation(operation);
        }
    }

    private boolean isOutOnly(BindingOperation bindingOperation) {
        if (bindingOperation.getBindingInput() != null && bindingOperation.getBindingOutput() == null) {
            return true;
        } else if (bindingOperation.getBindingOutput() != null && bindingOperation.getBindingInput() == null) {
            throw new IllegalArgumentException("Outonly services are not supported.");
        }

        return false;

    }

    private String getTargetNamesapce() {
        return httpBinding.getQName().getNamespaceURI();
    }

    private AxisOperation createAxisOperation(String name, String nsUrl, boolean outOnly) {
        AxisOperation operation;

        if (outOnly) {
            operation = new OutOnlyAxisOperation();
        } else {
            operation = new OutInAxisOperation();
        }

        operation.setName(new QName(nsUrl, name));
        return operation;
    }

    public class HTTPBindingResponse{
        private MessageContext responseMessageContext;
        private MessageContext faultMessageContext;

        public HTTPBindingResponse(MessageContext responseMessageContext, MessageContext faultMessageContext){
            this.responseMessageContext = responseMessageContext;
            this.faultMessageContext = faultMessageContext;
        }

        public boolean isFault(){
            return faultMessageContext != null;
        }

        public MessageContext getReponseMessageContext(){
            return responseMessageContext;
        }

        public MessageContext getFaultMessageContext(){
            return faultMessageContext;
        }
    }
}
